package ar.unc.famaf.pln.giveselector;

import java.awt.geom.Rectangle2D;
import java.io.*;
import java.util.*;

import ar.unc.famaf.pln.giveselector.classifier.ContinuousClassifier;
import ar.unc.famaf.pln.giveselector.classifier.IClassifier;
import ar.unc.famaf.pln.giveselector.context.*;
import ar.unc.famaf.pln.giveselector.filters.*;
import ar.unc.famaf.pln.giveselector.pickers.*;
import ar.unc.famaf.pln.giveselector.selectors.*;

import give.comm.ClientMetadata;
import give.formula.*;
import give.nlgserver.*;
import give.nlgserver.discretize.*;
import give.questionnaire.QuestionnaireData;
import give.world.*;

/**
 * The SelectorGiveServer uses an DefaultSelector instead of a Generator to generate the utterances.
 * @author Alexandre Denis
 * @author Luciana Benotti
 */
public class SelectorGiveServer extends NlgSystem
{
	private int curFrame;
	private final int frameRate = 15; 	// 3s - How frequently the system reacts to DF's actions
	private int timeoutFrame;
	private int timeoutRate = 15; 		// 3s - The timeout depends on the distance to the next action. 
	private boolean initialized;

	private Context context;
	private IPicker picker;
	private List<IFilter> filters;
	static private ISelector selector;
	private IClassifier classifier;
	private SendingThread sender;
	private RegionDiscretizer discretizer;

	private Utterance lastSelectedUtterance;
	private World world;
	private List<Atom> plan = new ArrayList<Atom>();

	private final static int TIME_FACTOR = 200;		// The sender shows messages for MAX( msg.length * 200, MIN_TIME ) miliseconds
	private final static int MIN_TIME = 6000;
	private final boolean showShortTime = true;

	// was an activated alarm already grounded?
	public Boolean alarmGrounded = false;
	public List<String> unGroundedAlarmWarnings = new ArrayList<String>();
	
	// alarm warning
	public int alarmTrigger = 0;
	
	// status variables
	public Boolean dfwelcomed = false;
	public List<String> ungroundedWelcome = new ArrayList<String>();
	public int totalButtons;
	public int buttonsLeft;
    public String progressFeedback;
    public int buttonsLeftOld;
	
	// optimized planning
	public Boolean optimizedPlanning = true;
	public Boolean regionChanged = false;
	
	/**
	 * Creates a new SelectorGiveServer. The first time is invoked (when the first client is connected), 
	 * it creates the selector object passing as argument to it the action-utterances.bin.gz file 
	 * located at "datas/action-utterances-with-id-classified-performance.bin.gz".
	 * @param inst
	 * @param world
	 * @param discretizer
	 */
	public SelectorGiveServer(NlgServerInstance inst, World world, Discretizer discretizer)
	{
		super(inst, world, discretizer);
		if (inst==null)
		{
			selector = new ACLSelector(new File("datas/action-utterances-with-id-classified-performance.bin.gz"));
		}
		this.lastSelectedUtterance = null;
		this.world = world;
	}


	/**
	 *  Use this with versions 2.3.3 or newer
	 */
	@Override
	public void connectionEstablished(QuestionnaireData arg0, ClientMetadata arg1) throws NlgServerException
	{
		sender = new SendingThread(this, TIME_FACTOR, MIN_TIME);
		sender.start();
		sender.enqueue("Loading...", showShortTime);
		System.out.println("Loading...");
		initialized = false;
		curFrame = 0;
		timeoutFrame = 0;
		discretizer = new RegionDiscretizer(getWorld());

		// change the classifier here
		classifier = new ContinuousClassifier();

		// change the applied filters here
		filters = new ArrayList<IFilter>();
		//filters.add(new BackwardFilter());
		//filters.add(new OrientationFilter());
		//filters.add(new PFilter(classifier));

		// change the picker here
		//picker = new MemoryPicker();
		picker = new PMemoryPicker(classifier);
	}


	/**
	 * Computes a new plan.
	 */
	public List<Atom> computePlan()
	{
		List<Atom> ret = null;
		if (optimizedPlanning) 
		{
			ret = getOptimizedPlan();
		}
		else try
		{
			ret = getPlan(getWorld().getGoals());
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		return ret;
	}


	/**
	 * Computes how many frames will consider the next timeout to expire.
	 * This determines when the next timeout will expire.
	 * This depends on the distance between the DF and the
	 * next action he needs to perform.
	 */
	public int computeNextTimeout()
	{
		int frames = 20; 		// 4 seconds.
		double distance = -1.0;
		
		if (lastSelectedUtterance!=null)
		{	
			int maxFrames = 125; 	// 25 seconds.
			Atom nextAction = context.getImmediateFutureAction();
			if (nextAction != null)
			{
				String pred = nextAction.getPredicate();
				Position p2 = null;
	
				if (pred.startsWith("move"))
					p2 = getCenterOfRegion(nextAction.getArgumentAsString(1));
				else
					p2 = world.getPosition(getEntity(nextAction));
	
				Position p1 = world.getPlayerPosition();
				distance = p1.distance(p2);
				frames = (int) Math.min(maxFrames, Math.max(frames, Math.floor(30.0*distance)));
			}
		}
		else
			frames = 15;	// 3 seconds if the last sended mess was "go"

		System.out.println("Computed timeout: " + ((float) frames )/ 5f 
				+ " seconds, Frames: " + frames + ", d" + "Distance: " + distance);
		return frames;
	}


	 /**
     * Returns the entity referenced by the planStep.
     * PRE: fManPStep should be 'manipulate' or 'take'.
     * @return A String, the entity referenced
     */
	public static String getEntity(Atom manPS) {
		String entity = null;
		String predicate = manPS.getPredicate();

		// Case manPStep=manipulate-stateless-a(p) (where a is the entity and p is the position)
		if (predicate.startsWith("manipulate-stateless-"))
			entity = predicate.substring("manipulate-stateless-".length());
		// Case manPStep=take-a(p) (where a is the entity and p is the position)
		else if (predicate.startsWith("take-"))
			entity = predicate.substring("take-".length());
		else // Caso manPStep=manipulate(a,S1,S2,p), manipulate-stateless(a,b,S1,S2,p)
			entity = manPS.getArgumentAsString(0);
		return entity;
	}


	/**
	 * Computes the center of a given region by creating a RegionDiscretizer and
     * getting a bounding box from it, then computing the center of that box 
     */
    public Position getCenterOfRegion(String region) {
        RegionDiscretizer.RegionInfo Info = discretizer.getRegionInformation(region);
        Rectangle2D box = Info.getBoundingBox();
        Position center = discretizer.getApproximatePosition(region);
        center.setX(box.getCenterX());
        center.setZ(box.getCenterY());
        return center;
    }


	/**
	 * Main status callback.
	 */
	@Override
	public void handleStatusInformation(Position position, Orientation orientation, List<String> visibleThings) throws NlgServerException
	{
		if (!initialized)
		{
			initialized = true;
			context = new Context(discretizer.getPositionName(position));
			plan = computePlan();
			context.setFutureActions(plan);
			buttonsLeft = context.getFutureManipulates().size();
			totalButtons = context.getFutureManipulates().size();
		}

		String currentRegion = discretizer.getPositionName(position);
		//System.out.println("CURRENT REGION: "+currentRegion);		
		if (!regionChanged)
			regionChanged = context.updateMove(currentRegion);

		if (curFrame >= frameRate)
		{
			context.setOrientation(getWorld().getPlayerOrientation());
			
			if (!unGroundedAlarmWarnings.isEmpty())
			{
				sendPlusFeedback(unGroundedAlarmWarnings.remove(0), showShortTime);
				if (unGroundedAlarmWarnings.isEmpty() && ungroundedWelcome.isEmpty())
					sendPlusFeedback(verbaliseAction(), !showShortTime);
			}
			else if (!ungroundedWelcome.isEmpty())
			{
				sendPlusFeedback(ungroundedWelcome.remove(0), showShortTime);
				if (ungroundedWelcome.isEmpty())
					sendPlusFeedback(verbaliseAction(), !showShortTime);
			}

			if (regionChanged)
			{
				System.out.println("\nRegionChanged!");
				regionChanged = false;
				timeoutFrame = 0;
				Atom expectedMove = context.getImmediateFutureAction();
				Atom performedMove = context.getImmediatePastAction();

				if (expectedMove.equals(performedMove))
				{
					System.out.println("Correct move!");
					plan.remove(0);
					context.setFutureActions(plan);
					sendIfFinishedWithUtt(performedMove);
				}
				else
				{
					System.out.println("Incorrect move!");
					plan = computePlan();
					context.setFutureActions(plan);
					if (ungroundedWelcome.isEmpty() && unGroundedAlarmWarnings.isEmpty())
					{
						sender.clear();
						sendPlusFeedback(verbaliseAction(), !showShortTime);
					}
				}
				//logResult();
			}
			
			curFrame = 0;
		}
		else 
		{
			curFrame++;
			if (ungroundedWelcome.isEmpty() && unGroundedAlarmWarnings.isEmpty())
				timeoutFrame++;
		}
		//System.out.println(timeoutFrame);
		//System.out.println(timeoutRate);
		//System.out.println(curFrame);
		//System.out.println(frameRate + "\n");

		// begin welcome
		if (!dfwelcomed)
		{
			sender.clear();
			timeoutFrame = 0;
			ungroundedWelcome = WelcomeMessages();
			dfwelcomed = true;
		}

		// begin alarm grounding
		if (anActivatedAlarmIsVisible(position, orientation, visibleThings) && !alarmGrounded)
		{
			sender.clear();
			timeoutFrame = 0;
			alarmGrounded = true;
			unGroundedAlarmWarnings = AlarmGroundingUtterances();
			curFrame = frameRate;
		}

		// alarm too close warning
		if (anActivatedAlarmIsTooClose(position, orientation) && alarmGrounded && unGroundedAlarmWarnings.isEmpty())
		{
			// System.out.println("we are too close to alarm, get a bit further");
			if (alarmTrigger==0)
			{
				sender.clear();
				System.out.println("\nWarning Alarm!");
				timeoutFrame = 0;
				alarmTrigger = 150;
				sendPlusFeedback("slowly, we are too close to alarm", showShortTime);
				curFrame = 0;
			}
			else alarmTrigger--;
		}

		// Timeout
		if (timeoutFrame >= timeoutRate)
		{
			System.out.println("\nTimeout!");
			timeoutFrame = 0;
			sender.clear();
			sendPlusFeedback(verbaliseAction(), !showShortTime);
		}

	}


	/**
	 * Sends the message to the player adding at the end the feedback about progress. 
	 * @param message
	 */
	public void sendPlusFeedback(String message, boolean showShortTime)
	{
		String flushRight = ""+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+(char)160+"";
		//System.out.println(message.length());
		if (message.length()>40)
			progressFeedback = "\n" + flushRight + (totalButtons-buttonsLeft) + "/" + totalButtons;
		else
			progressFeedback = "\n"+(char)160+"\n" + flushRight+ (totalButtons-buttonsLeft) + "/" + totalButtons;
		sender.enqueue(message+progressFeedback, showShortTime);
	}

	/**
	 * Obtains all the manipulate and take plan actions that are in the plannedActions list
	 * @param plannedActions
	 * @return the manipulate and take actions from plannedActions
	 */
	public static List<Atom> getFutureActions(List<Atom> plannedActions)
	{
		List<Atom> ret = new ArrayList<Atom>();
		for (Atom action:plannedActions)
		{
			if (action.getPredicate().contains("manipulate") || action.getPredicate().contains("take"))
			{
				ret.add(action);
			}
		}
		//ret = ACLSelector.cleanPlannerActions(ret);
		return ret;
	}


	/**
	 * Obtains all move type actions that are before the first manipulate or take action.
	 * @param plannedActions
	 * @return a list with the first move actions from plannedActions.
	 */
	public static List<Atom> getFutureMoveUntilAction(List<Atom> plannedActions)
	{
		List<Atom> ret = new ArrayList<Atom>();
		if (!(plannedActions == null || plannedActions.isEmpty()))
		{
			Atom action = plannedActions.get(0);
			int i = 0;
			while (!(action.getPredicate().contains("manipulate") || (action.getPredicate().contains("take"))))
			{
				ret.add(action);
				i++;
				action = plannedActions.get(i);
			}
		}
		return ret;
	}

	/**
	 * Uses the planner to recompute the first fragment of the raw plan. 
	 * @return A list of atoms. A optimized plan in the sense that its 
	 * move actions until the first manipulate or take action are the optims.
	 */
	public List<Atom> getOptimizedPlan() 
	{
		List<Atom> optimizedMoves = new ArrayList<Atom>();
		try
		{
			List<Atom> optimizedPlan = getPlan(getWorld().getGoals());
			//System.out.println("ORIGINAL plan :"+optimizedPlan);
			if (optimizedPlan!=null)
			{
				List<Atom> actions = getFutureActions(optimizedPlan);
				Atom nextAtom = actions.get(0);
				//System.out.println("Next manipulate: "+nextAtom);
				Action nextAction = world.findAction(nextAtom);
				//System.out.println("Next manipulate: "+nextAction);
				
				World goalState = new World();
				world.copyTo(goalState);
				nextAction.apply(nextAtom, goalState);
				
				List<Atom> goalAtoms = new ArrayList<Atom>(goalState.getTrueAtoms());
				Collection<Atom> positions = new ArrayList<Atom>();
				for (Atom atom:goalAtoms)
				{
					if ((atom.getPredicate().equals("position")) || (atom.getPredicate().equals("other-position")))
						positions.add(atom);
				}
				goalAtoms.removeAll(positions);
				List<Formula> goalFormula = new ArrayList<Formula>(goalAtoms);
				
			    List<Atom> optimizedUntilAction = getPlan(goalFormula);
			    //System.out.println("OPTIMIZED until action: "+optimizedUntilAction);
			    
			    optimizedMoves = getFutureMoveUntilAction(optimizedUntilAction);
			    List<Atom> oldMoves = getFutureMoveUntilAction(optimizedPlan);
			    optimizedPlan.removeAll(oldMoves);
			    optimizedMoves.addAll(optimizedPlan);
			    //System.out.println("OPTIMIZED plan: "+optimizedMoves);
			}
			else
				sender.enqueue("click and win :)", true);
			
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		
		return optimizedMoves;
		
	}
	
	
	@Override
	public void handleAction(Atom action, List<Formula> arg1) throws NlgServerException
	{
		Atom actionPerformed = new Atom(action.getPredicate(), action.getArguments().get(0));
		System.out.println("\nDF: " + actionPerformed);

		List<Atom> futureManipulates = context.getFutureManipulates();
		Atom expectedAction = null;

		if (!futureManipulates.isEmpty())
			expectedAction = futureManipulates.get(0);
		//System.out.println("Expected Action: "+expectedAction);
		//System.out.println("Action Performed: "+actionPerformed);
		
		//I have to replan because the button might have been pressed from a non button region
		plan = computePlan();
		context.setFutureActions(plan);
		buttonsLeftOld = buttonsLeft;
		buttonsLeft = context.getFutureManipulates().size();
		progressFeedback = "\n " + (totalButtons-buttonsLeft) + "/" + totalButtons;
		sender.clear();

		if (expectedAction!=null && expectedAction.equals(actionPerformed))
		{
			System.out.println("\nCorrect manipulate!\n");
			if (totalButtons-buttonsLeft==1)
				sendPlusFeedback("yup, one button done :D", showShortTime);
			else if (totalButtons-buttonsLeft==2)
				sendPlusFeedback("2 buttons done ;)", showShortTime);
			else sendPlusFeedback((totalButtons-buttonsLeft)+" done :)", showShortTime);

			sendIfFinishedWithUtt(actionPerformed);
		}
		else
		{
			System.out.println("Incorrect manipulate!");
			//sendPlusFeedback("no! that click was wrong!");
			//System.out.println(context.getImmediatePastAction());
			//System.out.println(actionPerformed);
			if (buttonsLeftOld+1 < buttonsLeft)
			{
				sendPlusFeedback("oops! that click reset part of the security", showShortTime);
				sendPlusFeedback("but we still can get to the trophy :)", showShortTime);	

			}
			else if (context.getImmediatePastAction().equals(actionPerformed))
			{
				sendPlusFeedback("it's crucial to press buttons only once", showShortTime);
				sendPlusFeedback("otherwise some button effects are undone", showShortTime);
			} 
			else
			{
				sendPlusFeedback("ups, that click wasn't in the sequence", showShortTime); 
				sendPlusFeedback("luckily it was harmless", showShortTime);
			}

			if (ungroundedWelcome.isEmpty() && unGroundedAlarmWarnings.isEmpty())
				sendPlusFeedback(verbaliseAction(), !showShortTime);
		}
		context.addPastAction(actionPerformed);
		//logResult();
	}

	/**
	 * Sends a new instruction only if the system is not sending the
	 * welcome or the grounding messages and DF has completed all
	 * the actions from the semantic of the last uttered instruction.
	 * @param performed: The action that has been performed by the DF.
	 */
	private void sendIfFinishedWithUtt(Atom performed)
	{
		if (ungroundedWelcome.isEmpty() && unGroundedAlarmWarnings.isEmpty() && lastSelectedUtterance!=null)
		{
			List<Atom> reaction = lastSelectedUtterance.getResponse().getDiscretizedActions();
			reaction = ACLSelector.cleanPlannerActions(reaction);
			System.out.println("Last Utt expected reaction: " + reaction);
			int n = reaction.size();
			int i = reaction.indexOf(performed);
			
			System.out.println("Completed: " + (i+1) + "/" + n + " expected actions");
			if (i == n-1)
			{
				System.out.println("Finished with last Utt semantic!");
				sender.clear();
				sendPlusFeedback(verbaliseAction(), !showShortTime);
			}
			System.out.println("");
		}
	}

	/**
	 * Logs the success or failure of the last uttered instruction.
	 */
	/*private void logResult()
	{
		if (!lastSelectedUtterances.isEmpty())
		{
			Atom pastAction = context.getImmediatePastAction();
			Utterance lastUtt = lastSelectedUtterances.get(lastSelectedUtterances.size() - 1);
			for(Utterance utt : lastSelectedUtterances)
			{
				List<Atom> expectedActionsFromCorpus = utt.getResponse().getDiscretizedActions();

				String resultMsg;
				if (expectedActionsFromCorpus.contains(pastAction))
					resultMsg = (lastUtt == utt ? "Success: " + utt : "Borrowed success: " + utt);
				else resultMsg = (lastUtt == utt ? "Failure: " + utt : "Borrowed failure: " + utt);

				//System.out.println(resultMsg);
				//System.out.println("Last action performed: " + context.getImmediatePastAction());
				//System.out.println("Expected actions: " + expectedActionsFromCorpus);

				log(resultMsg);
			}

			lastSelectedUtterances = new ArrayList<Utterance>();
		}
	}*/


	/**
	 * Verbalise the next action by selecting a suitable utterance.
	 */
	private String verbaliseAction()
	{
		String send = "";
		
		if (context.getNFutureAction(14)!=null)
		{
			Set<Utterance> utterances = selector.selectUtterances(getWorld().getName(), context);
							
			for(IFilter filter : filters)
				utterances = filter.filter(utterances, context);

			Utterance picked = picker.pick(utterances, context);
			
			if (picked == null)
			{
				System.out.println("Sending default 'go'");
				lastSelectedUtterance = null;
				send = "go";
			}
			else
			{
				send = picked.getText();
				lastSelectedUtterance = picked;
				timeoutRate = computeNextTimeout();
				log("Picked:" + picked.getId() + ":" + picked.getText());
			}
		}

		return send;
	}


	@Override
	public void connectionDisconnected()
	{
		sender.shutdown();
	}


	@Override
	public void handleDidNotUnderstand() throws NlgServerException
	{
		System.out.println("\nNot Understand!\n");
		if (ungroundedWelcome.isEmpty() && unGroundedAlarmWarnings.isEmpty())
		{
			plan = computePlan();
			context.setFutureActions(plan);
			sender.clear();
			sendPlusFeedback(verbaliseAction(), !showShortTime);
		}
	}


	// alarm warning methods, move to an independent class
	/**
	 * Returns all active alarms that are directly visible to the player, 
	 * activated and at a distance smaller than radius 
	 * @author Gaston Nieto
	 * @return A List<String> containing all the alarms that are directly visible by the player.
	 */
	public List<String> getVisibleAlarms(double radius, Position position, Orientation orientation, List<String> visibleThings) 
	{
		List<String> visibleAlarms = new ArrayList<String>();
		for (String objectName : visibleThings) 
		{
			double distance = world.getPosition(objectName).distance(position);
			if (world.getType(objectName).contentEquals("alarm") 
			&& world.getState(objectName).contentEquals("alarmed")
			&& distance < radius) {
				visibleAlarms.add(objectName);
			}
			
		}
		return visibleAlarms;
	}

	public boolean anActivatedAlarmIsVisible(Position position, Orientation orientation, List<String> visibleThings) 
	{
		return !getVisibleAlarms(3.2, position, orientation, visibleThings).isEmpty();
	}

	public List<String> getTooNearAlarms(double radius, Position position, Orientation orientation) 
	{
		List<String> tooNearAlarms = new ArrayList<String>(); 
		for(Atom atom : getWorld().getTrueAtoms())
			if (atom.getPredicate().equals("alarm"))
			{
				String alarm = atom.getArgumentAsString(0);
				if (world.getState(alarm).contentEquals("alarmed"))
				{
					double distance = world.getPosition(alarm).distance(position);
					if (distance <= radius)
						tooNearAlarms.add(alarm);
					//System.out.println(tooNearAlarms);
					//System.out.println(distance);
				}
			}
		return tooNearAlarms;
	}

	public boolean anActivatedAlarmIsTooClose(Position position, Orientation orientation) 
	{
		List<String> nearAlarms = getTooNearAlarms(2, position, orientation);
		//System.out.println("Alarmprefix that are near and activated: " +nearAlarms);
		return !nearAlarms.isEmpty();
	}

	
	public static List<String> AlarmGroundingUtterances() 
	{
		List<String> utterances = new ArrayList<String>();
		utterances.add("wait!");
		utterances.add("see that red area on the floor?");
		utterances.add("that's an activated alarm");
		utterances.add("if we step on it, we lose :(");
		utterances.add("so let's be careful, please");
		return utterances;
	}
	
	private List<String> WelcomeMessages()
	{
		List<String> utterances = new ArrayList<String>();
		utterances.add("Hi! that was the tutorial");
		utterances.add("I'm your game partner");
		utterances.add("Our goal is to steal a golden trophy");
		utterances.add("without activating alarms");
		utterances.add("I am at the house's control room");
		utterances.add("where I can monitor the security system");
		utterances.add("To disable security and get the trophy");
		utterances.add("we need to press the right buttons");
		utterances.add("If we don't make mistakes we'll press "+totalButtons);
		utterances.add("See the numbers below, on the right?");
		utterances.add("They show we pressed "+(totalButtons-buttonsLeft)+" out of "+totalButtons+" buttons");
		utterances.add("they'll be updated as we advance");
		utterances.add("let's go for the trophy!");
		return utterances;
	}

// needed by 2.3.0
//	@Override
//	public void handleExternalEvent(String arg0, Map<String, String> arg1) throws NlgServerException
//	{
//		
//	}
	
}
